第17天,聚餐吃太晚,差點來不及發文,沒想到這變成我每天最重要的小事了。
在JS中我們可以很方便的替物件增加屬性和方法,但卻難在不改動某個函數原始碼的狀態下,給該函數添加額外功能。
很多時候我們不會想要去改動某個函數內部,有可能這個函數相當龐大,內容可能是過去好幾任工程是增修過的,在不改變原始碼的情況下,我們其實可以通過存放原引用的方式(像裝飾者模式(上)最後範例一樣)就可以改寫某個函數:
var a = function() {
console.log(1);
};
var _a = a;
a = function() {
_a();
console.log(2);
};
a();
實際上的應用也是有這種狀況的,例如想給window綁定onload事件的話,因為不確定這個事件是否已經有人先綁定過了,所以為了避免覆蓋掉之前的函數,我們可以這麼做:
window.onload = function() {
alert(1);
};
var _onload = window.onload || function() {};
window.onload = function() {
_onload();
alert(2);
};
這樣的寫法仍然是符合開放-封閉原則,過程中我們並沒有去更動到原始的function。但這樣仍會有些問題:必須維護_onload、_a這些中間變數,如果裝飾函數越來越多,這樣的變數數量可能會過多。再來是this被更動的問題,第二次呼叫的this可能不是該方法應該指定用的物件,例如以下這狀況:
var _getElementById = document.getElementById;
document.getElementById = function(id) {
alert(1);
return _getElementById(id);
};
var btn = document.getElementById('test');
這段會拋出錯誤訊息,原因是因為getElementById這個方法要求this指定的物件要是document,現在this指的是window。或許可以這麼改:
var _getElementById = document.getElementById;
document.getElementById = function(id) {
alert(1);
return _getElementById.apply(document, id);
};
但這樣顯然不是很方便,我們接下來用AOP來給函數動態增加功能。
AOP(面向導向程式設計)簡單來說就是把跟核心業務邏輯模組無關的功能抽離,再通過動態加入的方式參入整個業務邏輯中。在JS實作中會在Function的原型物件裡增加before跟after函數,讓before、after函數當作裝飾者:
Function.prototype.before = function(fn) {
var _self = this;
return function() {
fn.apply(this, arguments);
return _self.apply(this, arguments);
}
};
Function.prototype.after = function(fn) {
var _self = this;
return function() {
var ret = _self.apply(this, arguments);
fn.apply(this, arguments);
return ret;
}
};
實際使用看看
document.getElementById = document.getElementById.before(function() {
alert(1);
});
$('<button>').attr('id', 'test').appendTo('body');
var btn = document.getElementById('test');
console.log(btn);
再回到onload例子上,就會發現方便很多
window.onload = function() {
console.log(1);
};
window.onload = (window.onload || function() {}).after(function() {
console.log(2);
}).after(function() {
console.log(3);
}).after(function() {
console.log(4);
});
我們舉個例子來實際展示分離業務邏輯程式碼與資料統計程式碼。我們要做一個登入按鈕,點擊後會跳出登入畫面,同時要進行資料回傳至server,來統計多少使用者點擊這個登入按鈕:
(function() {
$('<body>').text('Login').attr('tag', 'login')
.appendTo('body')
.click(showLogin);
})();
function showLogin() {
console.log('打開登入畫面');
log($(this).attr('tag'));
}
function log(tag) {
console.log('回傳tag:' + tag);
//省略傳server部分
}
上述例子中showLogin裡面不僅要顯示畫面,還要負責回傳log,這樣兩種層面的功能現在耦合在一起。現在我們用AOP分離的方式修改:
Function.prototype.after = function(fn) {
var _self = this;
return function() {
var ret = _self.apply(this, arguments);
fn.apply(this, arguments);
return ret;
}
};
var $btn;
(function() {
$btn = $('<body>').text('Login').attr('tag', 'login')
.appendTo('body');
})();
//把原本的log那段拿掉
function showLogin() {
console.log('打開登入畫面');
}
//想像成元素本身直接綁定此事件一樣
function log() {
console.log('回傳tag:' + $(this).attr('tag'));
//省略傳server部分
}
showLogin = showLogin.after(log);
//如果一開始就先綁定了showLogin的話,showLogin會是舊的,因此要在最後再綁定上去
$btn.click(showLogin);
我們除了一般的增加職責以外還可以在裡面做些判斷,一樣舉例來解釋,還記得我們在策略模式中所寫的檢查表單並送出的範例嗎,我們現在要onSubmit中除了檢查表單,還要ajax送出表單(如果你看過策略模式的話,整段範例只要注意onSubmit函數就好):
var $form = getForm();
$form.submit(onSubmit);
function onSubmit() {
var form = this;
var errorMsg = myFormValidator(form);
if (errorMsg) {
alert(errorMsg);
return false;
}
ajax( /*省略*/ );
}
var strategies = {
inNotEmpty: function(val, errorMsg) {
if (val === '') {
return errorMsg;
}
},
minLength: function(val, length, errorMsg) {
if (val.length < length) {
return errorMsg;
}
},
isMobileNumber: function(val, errorMsg) {
if (!/^[09]{2}[0-9]{8}$/.test(val)) {
return errorMsg;
}
}
};
function myFormValidator(form) {
var validator = new Validator();
validator.add(form.userName.value, [{
strategy: 'inNotEmpty',
errorMsg: '使用者名稱不為空'
}, {
strategy: 'minLength:6',
errorMsg: '使用者名稱位數不得少於6'
}]);
validator.add(form.password.value, [{
strategy: 'minLength:6',
errorMsg: '密碼位數不得少於6'
}]);
validator.add(form.phoneNumber.value, [{
strategy: 'isMobileNumber',
errorMsg: '手機號碼錯誤'
}]);
var errorMsg = validator.start();
return errorMsg;
}
var Validator = function() {
this.cache = [];
};
Validator.prototype.add = function(item, rules) {
var self = this;
rules.forEach(function(rule) {
var strategySet = rule.strategy.split(':');
self.cache.push(function() {
var strategyName = strategySet.shift();
strategySet.unshift(item);
strategySet.push(rule.errorMsg);
return strategies[strategyName].apply(self, strategySet);
});
});
};
Validator.prototype.start = function() {
var errorMsg;
this.cache.some(function(validatorFunc) {
errorMsg = validatorFunc();
if (errorMsg) {
return true;
}
});
return errorMsg;
};
這個範例只修改了onSubmit裡面增加ajax那段,其它都跟策略模式那篇一樣。
在範例裡你就會發現在onSubmit中除了檢查規則還要ajax送出,那我們可以怎麼改呢?我們可以在before裡面增加判斷,如果回傳為false就不要繼續執行之後的動作:
Function.prototype.before = function(fn) {
var _self = this;
return function() {
if (fn.apply(this, arguments) === false) {
return;
}
return _self.apply(this, arguments);
}
};
之後就可以修改onSubmit函數
var $form = getForm();
onSubmit = onSubmit.before(check);
$form.submit(onSubmit);
function onSubmit() {
ajax( /*省略*/ );
}
function check() {
var form = this;
var errorMsg = myFormValidator(form);
if (errorMsg) {
alert(errorMsg);
return false;
}
}
//以下內容都跟上一個範例一樣
var strategies = {
inNotEmpty: function(val, errorMsg) {
if (val === '') {
return errorMsg;
}
},
minLength: function(val, length, errorMsg) {
if (val.length < length) {
return errorMsg;
}
},
isMobileNumber: function(val, errorMsg) {
if (!/^[09]{2}[0-9]{8}$/.test(val)) {
return errorMsg;
}
}
};
function myFormValidator(form) {
var validator = new Validator();
validator.add(form.userName.value, [{
strategy: 'inNotEmpty',
errorMsg: '使用者名稱不為空'
}, {
strategy: 'minLength:6',
errorMsg: '使用者名稱位數不得少於6'
}]);
validator.add(form.password.value, [{
strategy: 'minLength:6',
errorMsg: '密碼位數不得少於6'
}]);
validator.add(form.phoneNumber.value, [{
strategy: 'isMobileNumber',
errorMsg: '手機號碼錯誤'
}]);
var errorMsg = validator.start();
return errorMsg;
}
var Validator = function() {
this.cache = [];
};
Validator.prototype.add = function(item, rules) {
var self = this;
rules.forEach(function(rule) {
var strategySet = rule.strategy.split(':');
self.cache.push(function() {
var strategyName = strategySet.shift();
strategySet.unshift(item);
strategySet.push(rule.errorMsg);
return strategies[strategyName].apply(self, strategySet);
});
});
};
Validator.prototype.start = function() {
var errorMsg;
this.cache.some(function(validatorFunc) {
errorMsg = validatorFunc();
if (errorMsg) {
return true;
}
});
return errorMsg;
};
裝飾者模式跟代理模式結構上看起來頗像,都替物件提供一定程度的間接引用。
代理模式有點像你多了一個經紀人的感覺,你本身的功能不會改變,但經紀人可以替你處理很多事。
而裝飾者模式就像你本身身上多加裝許多裝備,當然本身功能不會變,但是你最後就是可以多做一些事情。
以上就是裝飾者模式。